
#include "apis.hpp"
#include "keytable.hpp"
#include "pch.hpp"
#include "url_regex.hpp"
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>

auto get_config_integer(std::bind(get_config<int>, g_key_file_get_integer, _1,
                                  _2, _3));
auto get_config_string(std::bind(get_config<char *>, g_key_file_get_string, _1,
                                 _2, _3));
auto get_config_double(std::bind(get_config<double>, g_key_file_get_double, _1,
                                 _2, _3));

char *get_user_shell_with_fallback() {
    if (char const *env = g_getenv("SHELL")) {
        if (!((env != NULL) && (env[0] == '\0'))) {
            return g_strdup(env);
        }
    }

    if (char *command = vte_get_user_shell()) {
        if (!((command != NULL) && (command[0] == '\0'))) {
            return command;
        }
    }

    return g_strdup("/bin/sh");
}

void load_config(GtkWindow *window, VteTerminal *vte, GtkWidget *scrollbar,
                 GtkWidget *hbox, ConfigInfo *info, char **icon,
                 bool *show_scrollbar) {
    std::string const default_path = "/terminal-again/config";
    GKeyFile *config = g_key_file_new();
    GError *error = nullptr;

    gboolean loaded = FALSE;

    if (info->config_file) {
        loaded = g_key_file_load_from_file(config, info->config_file,
                                           G_KEY_FILE_NONE, &error);
        if (!loaded) {
            g_printerr("%s parsing failed: %s\n", info->config_file,
                       error->message);
        }
    }

    if (!loaded) {
        loaded = g_key_file_load_from_file(
            config, (g_get_user_config_dir() + default_path).c_str(),
            G_KEY_FILE_NONE, &error);
        if (!loaded) {
            g_printerr("%s parsing failed: %s\n",
                       (g_get_user_config_dir() + default_path).c_str(),
                       error->message);
        }
    }

    for (char const *const *dir = g_get_system_config_dirs(); !loaded && *dir;
         dir++) {
        loaded = g_key_file_load_from_file(
            config, (*dir + default_path).c_str(), G_KEY_FILE_NONE, &error);
        if (!loaded) {
            g_printerr("%s parsing failed: %s\n", (*dir + default_path).c_str(),
                       error->message);
        }
    }

    if (loaded) {
        set_config(window, vte, scrollbar, hbox, info, icon, show_scrollbar,
                   config);
    }
    g_key_file_free(config);
}

void set_config(GtkWindow *window, VteTerminal *vte, GtkWidget *scrollbar,
                GtkWidget *hbox, ConfigInfo *info, char **icon,
                bool *show_scrollbar_ptr, GKeyFile *config) {
    auto cfg_bool = [config](char const *key, gboolean value) {
        return get_config<gboolean>(g_key_file_get_boolean, config, "options",
                                    key)
            .value_or(value);
    };

    vte_terminal_set_scroll_on_output(vte, cfg_bool("scroll_on_output", FALSE));
    vte_terminal_set_scroll_on_keystroke(vte,
                                         cfg_bool("scroll_on_keystroke", TRUE));
    vte_terminal_set_audible_bell(vte, cfg_bool("audible_bell", FALSE));
    vte_terminal_set_mouse_autohide(vte, cfg_bool("mouse_autohide", FALSE));
    g_object_set(vte, "allow-bold", cfg_bool("allow_bold", TRUE),
                 nullptr); // Use g_object_set to avoid deprecation warning.
    vte_terminal_search_set_wrap_around(vte, cfg_bool("search_wrap", TRUE));
    vte_terminal_set_allow_hyperlink(vte, cfg_bool("hyperlinks", FALSE));
    vte_terminal_set_bold_is_bright(vte, cfg_bool("bold_is_bright", TRUE));
    vte_terminal_set_cell_height_scale(
        vte, get_config_double(config, "options", "cell_height_scale")
                 .value_or(1.0));
    vte_terminal_set_cell_width_scale(
        vte,
        get_config_double(config, "options", "cell_width_scale").value_or(1.0));
    vte_terminal_set_enable_bidi(vte, cfg_bool("bidi", FALSE));
    vte_terminal_set_enable_shaping(vte, cfg_bool("arabic_shaping", FALSE));
    info->dynamic_title = cfg_bool("dynamic_title", TRUE);
    info->urgent_on_bell = cfg_bool("urgent_on_bell", TRUE);
    info->clickable_url = cfg_bool("clickable_url", TRUE);
    info->clickable_url_ctrl = cfg_bool("clickable_url_ctrl", FALSE);
    info->size_hints = cfg_bool("size_hints", FALSE);
    info->filter_unmatched_urls = cfg_bool("filter_unmatched_urls", TRUE);
    info->modify_other_keys = cfg_bool("modify_other_keys", FALSE);
    info->fullscreen = cfg_bool("fullscreen", TRUE);
    info->smart_copy = cfg_bool("smart_copy", FALSE);
    info->dpi_aware = cfg_bool("dpi_aware", FALSE);
    info->copy_mouse_selection = cfg_bool("copy_mouse_selection", FALSE);
    info->font_scale = vte_terminal_get_font_scale(vte);

    g_free(info->browser);
    info->browser = nullptr;

    if (auto s = get_config_string(config, "options", "browser")) {
        info->browser = *s;
    } else {
        info->browser = g_strdup(g_getenv("BROWSER"));
    }

    if (!info->browser) {
        info->browser = g_strdup("xdg-open");
    }

    if (info->clickable_url) {
        info->tag = vte_terminal_match_add_regex(
            vte,
            vte_regex_new_for_match(url_regex, (gssize)strlen(url_regex),
                                    PCRE2_MULTILINE | PCRE2_NOTEMPTY, nullptr),
            0);
        vte_terminal_match_set_cursor_name(vte, info->tag, "hand");
    } else if (info->tag != -1) {
        vte_terminal_match_remove(vte, info->tag);
        info->tag = -1;
    }

    if (auto s = get_config_string(config, "options", "word_char_exceptions")) {
        vte_terminal_set_word_char_exceptions(vte, *s);
    }

    if (auto s = get_config_string(config, "options", "font")) {
        PangoFontDescription *font = pango_font_description_from_string(*s);
        info->font_size = pango_font_description_get_size_is_absolute(font)
                              ? 0
                              : pango_font_description_get_size(font);
        vte_terminal_set_font(vte, font);
        pango_font_description_free(font);
        if (info->dpi_aware) {
            adjust_font_size(vte, gtk_widget_get_window(GTK_WIDGET(vte)),
                             info->font_size);
        }
        g_free(*s);
    }

    if (auto i = get_config_integer(config, "options", "scrollback_lines")) {
        vte_terminal_set_scrollback_lines(vte, *i);
    }

    if (auto s = get_config_string(config, "options", "cursor_blink")) {
        if (!g_ascii_strcasecmp(*s, "system")) {
            vte_terminal_set_cursor_blink_mode(vte, VTE_CURSOR_BLINK_SYSTEM);
        } else if (!g_ascii_strcasecmp(*s, "on")) {
            vte_terminal_set_cursor_blink_mode(vte, VTE_CURSOR_BLINK_ON);
        } else if (!g_ascii_strcasecmp(*s, "off")) {
            vte_terminal_set_cursor_blink_mode(vte, VTE_CURSOR_BLINK_OFF);
        }
        g_free(*s);
    }

    if (auto s = get_config_string(config, "options", "cursor_shape")) {
        if (!g_ascii_strcasecmp(*s, "block")) {
            vte_terminal_set_cursor_shape(vte, VTE_CURSOR_SHAPE_BLOCK);
        } else if (!g_ascii_strcasecmp(*s, "ibeam")) {
            vte_terminal_set_cursor_shape(vte, VTE_CURSOR_SHAPE_IBEAM);
        } else if (!g_ascii_strcasecmp(*s, "underline")) {
            vte_terminal_set_cursor_shape(vte, VTE_CURSOR_SHAPE_UNDERLINE);
        }
        g_free(*s);
    }

    if (icon) {
        if (auto s = get_config_string(config, "options", "icon_name")) {
            *icon = *s;
        }
    }

    if (info->size_hints) {
        set_size_hints(GTK_WINDOW(window), vte);
    }

    bool show_scrollbar = false;
    if (auto s = get_config_string(config, "options", "scrollbar")) {
        // "off" is implicitly handled by default
        if (!g_ascii_strcasecmp(*s, "left")) {
            show_scrollbar = true;
            gtk_box_reorder_child(GTK_BOX(hbox), scrollbar, 0);
        } else if (!g_ascii_strcasecmp(*s, "right")) {
            show_scrollbar = true;
            gtk_box_reorder_child(GTK_BOX(hbox), scrollbar, -1);
        }
        g_free(*s);
    }
    if (show_scrollbar) {
        gtk_widget_show(scrollbar);
    } else {
        gtk_widget_hide(scrollbar);
    }
    if (show_scrollbar_ptr != nullptr) {
        *show_scrollbar_ptr = show_scrollbar;
    }

    g_object_set(gtk_settings_get_default(),
                 "gtk-application-prefer-dark-theme",
                 cfg_bool("gtk_dark_theme", FALSE), nullptr);

    load_theme(window, vte, config, info->hints);
}

void adjust_font_size(VteTerminal *vte, GdkWindow *window, int font_size = 18) {
    // Skip if the configuration specifies an absolute (pixel based) font size.
    if (!font_size) {
        return;
    }

    // If there is no window, that means we are at early startup reading the
    // config file, and the configure callback will be triggered later to
    // adjust the absolute size.
    if (!window) {
        return;
    }

    auto *screen = gtk_widget_get_screen(GTK_WIDGET(vte));
    auto *display = gdk_screen_get_display(screen);
    auto *monitor = gdk_display_get_monitor_at_window(display, window);

    GdkRectangle geometry;
    gdk_monitor_get_geometry(monitor, &geometry);

    static constexpr double mm_per_pt = 25.4 / 72.0; // DTP point.
    auto const height_mm = mm_per_pt * font_size;
    auto const height_px =
        geometry.height * height_mm / gdk_monitor_get_height_mm(monitor);

    PangoFontDescription *font =
        pango_font_description_copy_static(vte_terminal_get_font(vte));
    pango_font_description_set_absolute_size(font, height_px);
    vte_terminal_set_font(vte, font);
    pango_font_description_free(font);
}

void set_size_hints(GtkWindow *window, VteTerminal *vte) {
    static GdkWindowHints const wh =
        (GdkWindowHints)(GDK_HINT_RESIZE_INC | GDK_HINT_MIN_SIZE |
                         GDK_HINT_BASE_SIZE);
    int const char_width = (int)vte_terminal_get_char_width(vte);
    int const char_height = (int)vte_terminal_get_char_height(vte);
    int padding_left, padding_top, padding_right, padding_bottom;
    get_vte_padding(vte, &padding_left, &padding_top, &padding_right,
                    &padding_bottom);

    GdkGeometry hints;
    hints.base_width = char_width + padding_left + padding_right;
    hints.base_height = char_height + padding_top + padding_bottom;
    hints.min_width = hints.base_width;
    hints.min_height = hints.base_height;
    hints.width_inc = char_width;
    hints.height_inc = char_height;

    gtk_window_set_geometry_hints(GTK_WINDOW(window), NULL, &hints, wh);
}

void load_theme(GtkWindow *window, VteTerminal *vte, GKeyFile *config,
                HintInfo &hints) {
    std::array<GdkRGBA, 256> palette;
    char color_key[] = "color000";

    for (unsigned i = 0; i < palette.size(); i++) {
        snprintf(color_key, sizeof(color_key), "color%u", i);
        if (auto color = get_config_color(config, "colors", color_key)) {
            palette[i] = *color;
        } else if (i < 16) {
            palette[i].blue =
                (((i & 4) ? 0xc000 : 0) + (i > 7 ? 0x3fff : 0)) / 65535.0;
            palette[i].green =
                (((i & 2) ? 0xc000 : 0) + (i > 7 ? 0x3fff : 0)) / 65535.0;
            palette[i].red =
                (((i & 1) ? 0xc000 : 0) + (i > 7 ? 0x3fff : 0)) / 65535.0;
            palette[i].alpha = 0;
        } else if (i < 232) {
            unsigned const j = i - 16;
            unsigned const r = j / 36, g = (j / 6) % 6, b = j % 6;
            unsigned const red = (r == 0) ? 0 : r * 40 + 55;
            unsigned const green = (g == 0) ? 0 : g * 40 + 55;
            unsigned const blue = (b == 0) ? 0 : b * 40 + 55;
            palette[i].red = (red | red << 8) / 65535.0;
            palette[i].green = (green | green << 8) / 65535.0;
            palette[i].blue = (blue | blue << 8) / 65535.0;
            palette[i].alpha = 0;
        } else if (i < 256) {
            unsigned const shade = 8 + (i - 232) * 10;
            palette[i].red = palette[i].green = palette[i].blue =
                (shade | shade << 8) / 65535.0;
            palette[i].alpha = 0;
        }
    }

    vte_terminal_set_colors(vte, nullptr, nullptr, palette.data(),
                            palette.size());
    if (auto color = get_config_color(config, "colors", "foreground")) {
        vte_terminal_set_color_foreground(vte, &*color);
        vte_terminal_set_color_bold(vte, &*color);
    }
    if (auto color = get_config_color(config, "colors", "foreground_bold")) {
        vte_terminal_set_color_bold(vte, &*color);
    }
    if (auto color = get_config_color(config, "colors", "background")) {
        vte_terminal_set_color_background(vte, &*color);
        override_background_color(GTK_WIDGET(window), &*color);
    }
    if (auto color = get_config_color(config, "colors", "cursor")) {
        vte_terminal_set_color_cursor(vte, &*color);
    }
    if (auto color = get_config_color(config, "colors", "cursor_foreground")) {
        vte_terminal_set_color_cursor_foreground(vte, &*color);
    }
    if (auto color = get_config_color(config, "colors", "highlight")) {
        vte_terminal_set_color_highlight(vte, &*color);
    }

    if (auto s = get_config_string(config, "hints", "font")) {
        hints.font = pango_font_description_from_string(*s);
        g_free(*s);
    }

    hints.fg = get_config_cairo_color(config, "hints", "foreground")
                   .value_or(cairo_pattern_create_rgb(1, 1, 1));
    hints.bg = get_config_cairo_color(config, "hints", "background")
                   .value_or(cairo_pattern_create_rgb(0, 0, 0));
    hints.af = get_config_cairo_color(config, "hints", "active_foreground")
                   .value_or(cairo_pattern_create_rgb(0.9, 0.5, 0.5));
    hints.ab = get_config_cairo_color(config, "hints", "active_background")
                   .value_or(cairo_pattern_create_rgb(0, 0, 0));
    hints.border =
        get_config_cairo_color(config, "hints", "border").value_or(hints.fg);
    hints.padding =
        get_config_double(config, "hints", "padding", 5).value_or(2.0);
    hints.border_width =
        get_config_double(config, "hints", "border_width").value_or(1.0);
    hints.roundness =
        get_config_double(config, "hints", "roundness").value_or(1.5);
}

void get_vte_padding(VteTerminal *vte, int *left, int *top, int *right,
                     int *bottom) {
    GtkBorder border;
    gtk_style_context_get_padding(gtk_widget_get_style_context(GTK_WIDGET(vte)),
                                  gtk_widget_get_state_flags(GTK_WIDGET(vte)),
                                  &border);
    *left = border.left;
    *right = border.right;
    *top = border.top;
    *bottom = border.bottom;
}

std::optional<GdkRGBA> get_config_color(GKeyFile *config, char const *section,
                                        char const *key) {
    if (auto s = get_config_string(config, section, key)) {
        GdkRGBA color;
        if (gdk_rgba_parse(&color, *s)) {
            g_free(*s);
            return color;
        }
        g_printerr("invalid color string: %s\n", *s);
        g_free(*s);
    }
    return {};
}

void override_background_color(GtkWidget *widget, GdkRGBA *rgba) {
    GtkCssProvider *provider = gtk_css_provider_new();

    gchar *colorstr = gdk_rgba_to_string(rgba);
    char *css = g_strdup_printf("* { background-color: %s; }", colorstr);
    gtk_css_provider_load_from_data(provider, css, -1, nullptr);
    g_free(colorstr);
    g_free(css);

    gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
                                   GTK_STYLE_PROVIDER(provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    g_object_unref(provider);
}

std::optional<cairo_pattern_t *>
get_config_cairo_color(GKeyFile *config, char const *group, char const *key) {
    if (auto color = get_config_color(config, group, key)) {
        return cairo_pattern_create_rgba(color->red, color->green, color->blue,
                                         color->alpha);
    }
    return {};
}

void drag_data_received_cb(GtkWidget *, GdkDragContext *, int, int,
                           GtkSelectionData *data, unsigned, unsigned,
                           VteTerminal *vte) {
    std::string selection{
        reinterpret_cast<char const *>(gtk_selection_data_get_text(data))};
    std::string needle{"file://"};
    if (selection.compare(0, needle.length(), needle) == 0) {
        selection.erase(0, needle.length());
    }
    selection.erase(std::remove(selection.begin(), selection.end(), '\n'),
                    selection.end());
    vte_terminal_feed_child(vte, selection.c_str(), -1);
}

void exit_with_status(VteTerminal *, int status) {
    gtk_main_quit();
    exit(WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE);
}

void exit_with_success(VteTerminal *) {
    gtk_main_quit();
    exit(EXIT_SUCCESS);
}

gboolean entry_key_press_cb(GtkEntry *entry, GdkEventKey *event,
                            KeybindInfo *info) {
    guint const modifiers =
        event->state & gtk_accelerator_get_default_mod_mask();
    gboolean ret = FALSE;

    if (modifiers == GDK_CONTROL_MASK) {
        switch (event->keyval) {
        case GDK_KEY_bracketleft: ret = TRUE; break;
        }
    }
    switch (event->keyval) {
    case GDK_KEY_BackSpace:
        if (info->panel.mode == Overlay_Mode::O_M_UrlSelect &&
            info->panel.fulltext) {
            size_t slen = strlen(info->panel.fulltext);
            if (info->panel.fulltext != nullptr && slen > 0) {
                info->panel.fulltext[slen - 1] = '\0';
            }
            gtk_widget_queue_draw(info->panel.da);
        }
        break;
    case GDK_KEY_0:
    case GDK_KEY_1:
    case GDK_KEY_2:
    case GDK_KEY_3:
    case GDK_KEY_4:
    case GDK_KEY_5:
    case GDK_KEY_6:
    case GDK_KEY_7:
    case GDK_KEY_8:
    case GDK_KEY_9:
        if (info->panel.mode == Overlay_Mode::O_M_UrlSelect) {
            char const *const text = gtk_entry_get_text(entry);
            size_t len = strlen(text);
            free(info->panel.fulltext);
            info->panel.fulltext = g_strndup(text, len + 1);
            info->panel.fulltext[len] = (char)event->keyval;
            size_t urld = static_cast<size_t>(info->panel.url_list.size());
            size_t textd = strtoul(info->panel.fulltext, nullptr, 10);
            size_t url_dig = static_cast<size_t>(
                log10(static_cast<double>(info->panel.url_list.size())) + 1);
            size_t text_dig =
                static_cast<size_t>(log10(static_cast<double>(textd)) + 1);

            if (url_dig == text_dig ||
                textd > static_cast<size_t>(static_cast<double>(urld) / 10)) {
                launch_url(info->config.browser, info->panel.fulltext,
                           &info->panel);
                ret = TRUE;
            } else {
                gtk_widget_queue_draw(info->panel.da);
            }
        }
        break;
    case GDK_KEY_Tab:
        synthesize_keypress(GTK_WIDGET(entry), GDK_KEY_Down);
        return TRUE;
    case GDK_KEY_ISO_Left_Tab:
        synthesize_keypress(GTK_WIDGET(entry), GDK_KEY_Up);
        return TRUE;
    case GDK_KEY_Down:
        // this stops the down key from leaving the GtkEntry...
        event->hardware_keycode = 0;
        break;
    case GDK_KEY_Escape: ret = TRUE; break;
    case GDK_KEY_Return: {
        char const *const text = gtk_entry_get_text(entry);

        switch (info->panel.mode) {
        case Overlay_Mode::O_M_Search:   search(info->vte, text, false); break;
        case Overlay_Mode::O_M_Research: search(info->vte, text, true); break;
        case Overlay_Mode::O_M_Completion:
            vte_terminal_feed_child(info->vte, text, -1);
            break;
        case Overlay_Mode::O_M_UrlSelect:
            launch_url(info->config.browser, text, &info->panel);
            break;
        case Overlay_Mode::O_M_Hidden: break;
        }
        ret = TRUE;
    }
    }

    if (ret) {
        if (info->panel.mode == Overlay_Mode::O_M_UrlSelect) {
            gtk_widget_hide(info->panel.da);
            info->panel.url_list.clear();
            free(info->panel.fulltext);
            info->panel.fulltext = nullptr;
        }
        info->panel.mode = Overlay_Mode::O_M_Hidden;
        gtk_widget_hide(info->panel.entry);
        gtk_widget_grab_focus(GTK_WIDGET(info->vte));
    }
    return ret;
}

gboolean key_press_cb(VteTerminal *vte, GdkEventKey *event, KeybindInfo *info) {
    guint const modifiers =
        event->state & gtk_accelerator_get_default_mod_mask();

    if (info->config.fullscreen && event->keyval == GDK_KEY_F11 && !modifiers) {
        info->fullscreen_toggle(info->window);
        return TRUE;
    }

    if (info->select.mode != Vi_Mode::V_M_Insert) {
        if (modifiers == GDK_CONTROL_MASK) {
            switch (gdk_keyval_to_lower(event->keyval)) {
            case GDK_KEY_bracketleft:
                exit_command_mode(vte, &info->select);
                gtk_widget_hide(info->panel.da);
                gtk_widget_hide(info->panel.entry);
                info->panel.url_list.clear();
                break;
            case GDK_KEY_v:
                toggle_visual(vte, &info->select, Vi_Mode::V_M_VisualBlock);
                break;
            case GDK_KEY_Left:
                move_backward_blank_word(vte, &info->select);
                break;
            case GDK_KEY_Right:
                move_forward_blank_word(vte, &info->select);
                break;
            case GDK_KEY_u:
                move(vte, &info->select, 0,
                     -(vte_terminal_get_row_count(vte) / 2));
                break;
            case GDK_KEY_d:
                move(vte, &info->select, 0,
                     vte_terminal_get_row_count(vte) / 2);
                break;
            case GDK_KEY_b:
                move(vte, &info->select, 0,
                     -(vte_terminal_get_row_count(vte) - 1));
                break;
            case GDK_KEY_f:
                move(vte, &info->select, 0,
                     vte_terminal_get_row_count(vte) - 1);
                break;
            }
            return TRUE;
        }
        if (modifiers == GDK_SHIFT_MASK) {
            switch (event->keyval) {
            case GDK_KEY_Left:
                move_backward_word(vte, &info->select);
                return TRUE;
            case GDK_KEY_Right:
                move_forward_word(vte, &info->select);
                return TRUE;
            }
        }
        switch (event->keyval) {
        case GDK_KEY_Escape:
        case GDK_KEY_q:
            exit_command_mode(vte, &info->select);
            gtk_widget_hide(info->panel.da);
            gtk_widget_hide(info->panel.entry);
            info->panel.url_list.clear();
            break;
        case GDK_KEY_Page_Up:
            move(vte, &info->select, 0, -(vte_terminal_get_row_count(vte) - 1));
            break;
        case GDK_KEY_Page_Down:
            move(vte, &info->select, 0, (vte_terminal_get_row_count(vte) - 1));
            break;
        case GDK_KEY_Left:
        case GDK_KEY_h:     move(vte, &info->select, -1, 0); break;
        case GDK_KEY_Down:
        case GDK_KEY_j:     move(vte, &info->select, 0, 1); break;
        case GDK_KEY_Up:
        case GDK_KEY_k:     move(vte, &info->select, 0, -1); break;
        case GDK_KEY_Right:
        case GDK_KEY_l:     move(vte, &info->select, 1, 0); break;
        case GDK_KEY_b:     move_backward_word(vte, &info->select); break;
        case GDK_KEY_B:     move_backward_blank_word(vte, &info->select); break;
        case GDK_KEY_w:     move_forward_word(vte, &info->select); break;
        case GDK_KEY_W:     move_forward_blank_word(vte, &info->select); break;
        case GDK_KEY_e:     move_forward_end_word(vte, &info->select); break;
        case GDK_KEY_E:     move_forward_end_blank_word(vte, &info->select); break;
        case GDK_KEY_Home:  set_cursor_column(vte, &info->select, 0); break;
        case GDK_KEY_0:
            if (info->select.count == 0) {
                set_cursor_column(vte, &info->select, 0);
                break;
            }
            // fallthrough
        case GDK_KEY_1:
        case GDK_KEY_2:
        case GDK_KEY_3:
        case GDK_KEY_4:
        case GDK_KEY_5:
        case GDK_KEY_6:
        case GDK_KEY_7:
        case GDK_KEY_8:
        case GDK_KEY_9:
            info->select.count =
                info->select.count * 10 + (event->keyval - '0');
            break;
        case GDK_KEY_asciicircum:
            set_cursor_column(vte, &info->select, 0);
            move_first(vte, &info->select, std::not_fn(g_unichar_isspace));
            break;
        case GDK_KEY_dollar:
        case GDK_KEY_End:    move_to_eol(vte, &info->select); break;
        case GDK_KEY_g:
            move_to_row_start(vte, &info->select, first_row(vte));
            break;
        case GDK_KEY_G:
            move_to_row_start(vte, &info->select, last_row(vte));
            break;
        case GDK_KEY_H:
            move_to_row_start(vte, &info->select, top_row(vte));
            break;
        case GDK_KEY_M:
            move_to_row_start(vte, &info->select, middle_row(vte));
            break;
        case GDK_KEY_L:
            move_to_row_start(vte, &info->select, bottom_row(vte));
            break;
        case GDK_KEY_v:
            toggle_visual(vte, &info->select, Vi_Mode::V_M_Visual);
            break;
        case GDK_KEY_V:
            toggle_visual(vte, &info->select, Vi_Mode::V_M_VisualLine);
            break;
        case GDK_KEY_y: copy_selection(vte); break;
        case GDK_KEY_slash:
            overlay_show(&info->panel, Overlay_Mode::O_M_Search, vte);
            break;
        case GDK_KEY_question:
            overlay_show(&info->panel, Overlay_Mode::O_M_Research, vte);
            break;
        case GDK_KEY_n:
            for (long i = get_count(&info->select); i > 0; i--) {
                vte_terminal_search_find_next(vte);
            }
            vte_terminal_copy_primary(vte);
            break;
        case GDK_KEY_N:
            for (long i = get_count(&info->select); i > 0; i--) {
                vte_terminal_search_find_previous(vte);
            }
            vte_terminal_copy_primary(vte);
            break;
        case GDK_KEY_u:
            for (long i = get_count(&info->select); i > 0; i--) {
                search(vte, url_regex, false);
            }
            break;
        case GDK_KEY_U:
            for (long i = get_count(&info->select); i > 0; i--) {
                search(vte, url_regex, true);
            }
            break;
        case GDK_KEY_o: open_selection(info->config.browser, vte); break;
        case GDK_KEY_Return:
            open_selection(info->config.browser, vte);
            exit_command_mode(vte, &info->select);
            break;
        case GDK_KEY_x:
            if (!info->config.browser) {
                break;
            }
            find_urls(vte, &info->panel);
            gtk_widget_show(info->panel.da);
            overlay_show(&info->panel, Overlay_Mode::O_M_UrlSelect, nullptr);
            break;
        case GDK_KEY_plus:
        case GDK_KEY_KP_Add:      increase_font_scale(vte); break;
        case GDK_KEY_KP_Subtract:
        case GDK_KEY_minus:       decrease_font_scale(vte); break;
        case GDK_KEY_equal:
            reset_font_scale(vte, info->config.font_scale);
            break;
        }
        return TRUE;
    }
    if (modifiers == (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) {
        switch (gdk_keyval_to_lower(event->keyval)) {
        case GDK_KEY_plus: increase_font_scale(vte); return TRUE;
        case GDK_KEY_equal:
            reset_font_scale(vte, info->config.font_scale);
            return TRUE;
        case GDK_KEY_t: launch_in_directory(vte); return TRUE;
        case GDK_KEY_space:
        case GDK_KEY_nobreakspace: // shift-space on some keyboard layouts
            enter_command_mode(vte, &info->select);
            return TRUE;
        case GDK_KEY_x:
            enter_command_mode(vte, &info->select);
            find_urls(vte, &info->panel);
            gtk_widget_show(info->panel.da);
            overlay_show(&info->panel, Overlay_Mode::O_M_UrlSelect, nullptr);
            exit_command_mode(vte, &info->select);
            return TRUE;
        case GDK_KEY_c: copy_selection(vte); return TRUE;
        case GDK_KEY_v:
            vte_terminal_paste_clipboard(vte);
            return TRUE;
            // TODO: Not reload_config
            // case GDK_KEY_r:
            //     [&]() {
            //         // load_config(GTK_WINDOW(window), vte, scrollbar, hbox,
            //         //             &info.config, nullptr, nullptr);
            //     }();
            return TRUE;
        case GDK_KEY_l: vte_terminal_reset(vte, TRUE, TRUE); return TRUE;
        default:
            if (modify_key_feed(event, info, modify_table)) {
                return TRUE;
            }
        }
    } else if ((modifiers == (GDK_CONTROL_MASK | GDK_MOD1_MASK)) ||
               (modifiers ==
                (GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_SHIFT_MASK))) {
        if (modify_key_feed(event, info, modify_meta_table)) {
            return TRUE;
        }
    } else if (modifiers == GDK_CONTROL_MASK) {
        switch (gdk_keyval_to_lower(event->keyval)) {
        case GDK_KEY_Tab:
            overlay_show(&info->panel, Overlay_Mode::O_M_Completion, vte);
            return TRUE;
        case GDK_KEY_plus:
        case GDK_KEY_KP_Add:      increase_font_scale(vte); return TRUE;
        case GDK_KEY_minus:
        case GDK_KEY_KP_Subtract: decrease_font_scale(vte); return TRUE;
        case GDK_KEY_equal:
            reset_font_scale(vte, info->config.font_scale);
            return TRUE;
        case GDK_KEY_c:
            if (info->config.smart_copy &&
                vte_terminal_get_has_selection(vte)) {
                copy_selection(vte);
                return TRUE;
            }
            return FALSE;
        case GDK_KEY_v:
            if (info->config.smart_copy) {
                vte_terminal_paste_clipboard(vte);
                return TRUE;
            }
            return FALSE;
        default:
            if (modify_key_feed(event, info, modify_table)) {
                return TRUE;
            }
        }
    }
    return FALSE;
}

gboolean position_overlay_cb(GtkBin *overlay, GtkWidget *widget,
                             GdkRectangle *alloc) {
    GtkWidget *vte = gtk_bin_get_child(overlay);

    int const width = gtk_widget_get_allocated_width(vte);
    int const height = gtk_widget_get_allocated_height(vte);

    GtkRequisition req;
    gtk_widget_get_preferred_size(widget, nullptr, &req);

    alloc->x = width - req.width - 40;
    alloc->y = 0;
    alloc->width = std::min(width, req.width);
    alloc->height = std::min(height, req.height);

    return TRUE;
}

gboolean button_press_cb(VteTerminal *vte, GdkEventButton *event,
                         ConfigInfo const *info) {
    if (info->clickable_url && event->type == GDK_BUTTON_PRESS) {
        GdkModifierType state = static_cast<GdkModifierType>(0);
        gdk_event_get_state(reinterpret_cast<GdkEvent *>(event), &state);
        if (info->clickable_url_ctrl && (state & GDK_CONTROL_MASK) == 0) {
            return FALSE;
        }

        auto match = with_unique(
            vte_terminal_hyperlink_check_event(vte, (GdkEvent *)event), g_free);
        if (!match) {
            match = with_unique(check_match(vte, event), g_free);
        }
        if (!match) {
            return FALSE;
        }

        if (event->button == 1) {
            launch_browser(info->browser, match.get());
        } else if (event->button == 3) {
            GtkClipboard *clipboard =
                gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
            gtk_clipboard_set_text(clipboard, match.get(), -1);
        }

        return TRUE;
    }
    return FALSE;
}

void bell_cb(GtkWidget *vte, gboolean *urgent_on_bell) {
    if (*urgent_on_bell) {
        gtk_window_set_urgency_hint(GTK_WINDOW(gtk_widget_get_toplevel(vte)),
                                    TRUE);
    }
}

gboolean focus_cb(GtkWindow *window) {
    gtk_window_set_urgency_hint(window, FALSE);
    return FALSE;
}

void on_alpha_screen_changed(GtkWindow *window, GdkScreen *, void *) {
    GdkScreen *screen = gtk_widget_get_screen(GTK_WIDGET(window));
    GdkVisual *visual = gdk_screen_get_rgba_visual(screen);

    if (!visual) {
        visual = gdk_screen_get_system_visual(screen);
    }

    gtk_widget_set_visual(GTK_WIDGET(window), visual);
}

gboolean window_state_cb(GtkWindow *, GdkEventWindowState *event,
                         KeybindInfo *info) {
    if (event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) {
        info->fullscreen_toggle = gtk_window_unfullscreen;
    } else {
        info->fullscreen_toggle = gtk_window_fullscreen;
    }
    return FALSE;
}

void window_title_cb(VteTerminal *vte, gboolean *dynamic_title) {
    char const *const title =
        *dynamic_title ? vte_terminal_get_window_title(vte) : nullptr;
    gtk_window_set_title(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(vte))),
                         title ? title : "Terminal-Again");
}

void spawn_callback(VteTerminal *terminal, GPid pid, GError *error,
                    gpointer user_data) {
    if (!terminal) {
        return;
    }
    if (pid == -1) {
        g_printerr("the command failed to run: %s\n", error->message);
        g_clear_error(&error);
    }
}

void launch_url(char *browser, char const *text, SearchPanelInfo *info) {
    char *end;
    errno = 0;
    unsigned long id = strtoul(text, &end, 10);
    if (!errno && id && id <= info->url_list.size() && !*end) {
        launch_browser(browser, info->url_list[id - 1].url.get());
    } else {
        g_printerr("url hint invalid: %s\n", text);
    }
}

void synthesize_keypress(GtkWidget *widget, unsigned keyval) {
    GdkEvent new_event;

    new_event.key.type = GDK_KEY_PRESS;
    new_event.key.window = gtk_widget_get_parent_window(widget);
    new_event.key.send_event = TRUE;
    new_event.key.time = GDK_CURRENT_TIME;
    new_event.key.keyval = keyval;
    new_event.key.state = GDK_KEY_PRESS_MASK;
    new_event.key.length = 0;
    new_event.key.string = nullptr;
    new_event.key.hardware_keycode = 0;
    new_event.key.group = 0;

    gdk_event_put(&new_event);
}

void exit_command_mode(VteTerminal *vte, SelectInfo *select) {
    vte_terminal_set_cursor_position(vte, select->origin_col,
                                     select->origin_row);
    vte_terminal_connect_pty_read(vte);
    vte_terminal_unselect_all(vte);
    select->mode = Vi_Mode::V_M_Insert;
}

void toggle_visual(VteTerminal *vte, SelectInfo *select, Vi_Mode mode) {
    if (select->mode == mode) {
        select->mode = Vi_Mode::V_M_Command;
    } else {
        if (select->mode == Vi_Mode::V_M_Command) {
            vte_terminal_get_cursor_position(vte, &select->begin_col,
                                             &select->begin_row);
        }
        select->mode = mode;
    }
    update_selection(vte, select);
}

void move_backward_blank_word(VteTerminal *vte, SelectInfo *select) {
    move_backward(vte, select, std::not_fn(g_unichar_isspace));
}

void move_forward_blank_word(VteTerminal *vte, SelectInfo *select) {
    move_forward(vte, select, std::not_fn(g_unichar_isspace), false);
}

void move(VteTerminal *vte, SelectInfo *select, long col, long row) {
    long const end_col = vte_terminal_get_column_count(vte) - 1;

    long count = get_count(select);

    long cursor_col, cursor_row;
    vte_terminal_get_cursor_position(vte, &cursor_col, &cursor_row);

    VteCursorBlinkMode mode = vte_terminal_get_cursor_blink_mode(vte);
    vte_terminal_set_cursor_blink_mode(vte, VTE_CURSOR_BLINK_OFF);

    vte_terminal_set_cursor_position(
        vte, std::clamp(cursor_col + col * count, 0l, end_col),
        std::clamp(cursor_row + row * count, first_row(vte), last_row(vte)));

    update_scroll(vte);
    update_selection(vte, select);
    vte_terminal_set_cursor_blink_mode(vte, mode);
}

void search(VteTerminal *vte, char const *pattern, bool reverse) {
    auto terminal_search = reverse ? vte_terminal_search_find_previous
                                   : vte_terminal_search_find_next;

    VteRegex *regex = vte_terminal_search_get_regex(vte);
    if (regex) {
        vte_regex_unref(regex);
    }
    vte_terminal_search_set_regex(
        vte,
        vte_regex_new_for_search(pattern, (gssize)strlen(pattern),
                                 PCRE2_MULTILINE | PCRE2_CASELESS, nullptr),
        0);

    if (!terminal_search(vte)) {
        vte_terminal_unselect_all(vte);
        terminal_search(vte);
    }

    vte_terminal_copy_primary(vte);
}

void move_backward_word(VteTerminal *vte, SelectInfo *select) {
    move_backward(vte, select, is_word_char);
}

void move_forward_word(VteTerminal *vte, SelectInfo *select) {
    move_forward(vte, select, is_word_char, false);
}

void move_forward_end_word(VteTerminal *vte, SelectInfo *select) {
    move_forward(vte, select, is_word_char, true);
}

void move_forward_end_blank_word(VteTerminal *vte, SelectInfo *select) {
    move_forward(vte, select, std::not_fn(g_unichar_isspace), true);
}

void set_cursor_column(VteTerminal *vte, SelectInfo const *select,
                       long column) {
    long cursor_row;
    vte_terminal_get_cursor_position(vte, nullptr, &cursor_row);
    vte_terminal_set_cursor_position(vte, column, cursor_row);
    update_selection(vte, select);
}

std::unique_ptr<char, decltype(&g_free)>
get_text_range(VteTerminal *vte, long start_row, long start_col, long end_row,
               long end_col) {
    return {vte_terminal_get_text_range_format(vte, VTE_FORMAT_TEXT, start_row,
                                               start_col, end_row, end_col,
                                               nullptr),
            g_free};
}

template <typename F>
void move_first(VteTerminal *vte, SelectInfo *select, F is_match) {
    long cursor_col, cursor_row;
    vte_terminal_get_cursor_position(vte, &cursor_col, &cursor_row);

    long const end_col = vte_terminal_get_column_count(vte) - 1;

    auto content =
        get_text_range(vte, cursor_row, cursor_col, cursor_row, end_col);

    if (!content) {
        return;
    }

    long length;
    gunichar *codepoints =
        g_utf8_to_ucs4(content.get(), -1, nullptr, &length, nullptr);

    if (!codepoints) {
        return;
    }

    auto iter = std::find_if(codepoints, codepoints + length, is_match);
    if (iter != codepoints + length) {
        vte_terminal_set_cursor_position(vte, iter - codepoints, cursor_row);
        update_selection(vte, select);
    }

    g_free(codepoints);
}

void move_to_eol(VteTerminal *vte, SelectInfo *select) {
    long cursor_row;
    vte_terminal_get_cursor_position(vte, nullptr, &cursor_row);

    long const end_col = vte_terminal_get_column_count(vte) - 1;

    auto content = get_text_range(vte, cursor_row, 0, cursor_row, end_col);

    if (!content) {
        return;
    }

    long length;
    gunichar *codepoints =
        g_utf8_to_ucs4(content.get(), -1, nullptr, &length, nullptr);

    if (!codepoints) {
        return;
    }

    auto iter = std::find(codepoints, codepoints + length, '\n');
    set_cursor_column(vte, select, std::max(iter - codepoints - 1l, 0l));

    g_free(codepoints);
}

gboolean draw_cb(DrawCallbackInfo const *info, cairo_t *cr) {
    if (!info->panel->url_list.empty()) {
        char buffer[std::numeric_limits<unsigned>::digits10 + 1];

        int padding_left, padding_top, padding_right, padding_bottom;
        long const cw = vte_terminal_get_char_width(info->vte);
        long const ch = vte_terminal_get_char_height(info->vte);
        PangoFontDescription const *desc =
            info->hints->font ? info->hints->font
                              : vte_terminal_get_font(info->vte);
        size_t len = info->panel->fulltext == nullptr
                         ? 0
                         : strlen(info->panel->fulltext);

        cairo_set_line_width(cr, 1);
        cairo_set_source_rgb(cr, 0, 0, 0);
        cairo_stroke(cr);

        get_vte_padding(info->vte, &padding_left, &padding_top, &padding_right,
                        &padding_bottom);

        for (unsigned i = 0; i < info->panel->url_list.size(); i++) {
            UrlData const &data = info->panel->url_list[i];
            long const x = data.col * cw + padding_left;
            long const y = data.row * ch + padding_top;
            bool active = false;

            snprintf(buffer, sizeof(buffer), "%u", i + 1);
            if (len) {
                active = strncmp(buffer, info->panel->fulltext, len) == 0;
            }

            if (!info->filter_unmatched_urls || active || len == 0) {
                draw_marker(cr, desc, info->hints, x, y, buffer, active);
            }
        }
    }

    return FALSE;
}

long get_count(SelectInfo *select) {
    long count = 1;
    if (select->count > 0) {
        count = select->count;
        select->count = 0;
    }
    return count;
}

void draw_marker(cairo_t *cr, PangoFontDescription const *desc,
                 HintInfo const *hints, long x, long y, char const *msg,
                 bool active) {
    cairo_text_extents_t ext;
    int width, height;

    cairo_text_extents(cr, msg, &ext);
    PangoLayout *layout = pango_cairo_create_layout(cr);
    pango_layout_set_font_description(layout, desc);
    pango_layout_set_text(layout, msg, -1);
    pango_layout_get_size(layout, &width, &height);

    draw_rectangle(
        cr, static_cast<double>(x), static_cast<double>(y),
        static_cast<double>(width) / PANGO_SCALE + hints->padding * 2,
        static_cast<double>(height) / PANGO_SCALE + hints->padding * 2,
        hints->roundness);
    cairo_set_source(cr, hints->border);
    cairo_set_line_width(cr, hints->border_width);
    cairo_stroke_preserve(cr);
    cairo_set_source(cr, active ? hints->ab : hints->bg);
    cairo_fill(cr);

    cairo_new_path(cr);
    cairo_move_to(cr, static_cast<double>(x) + hints->padding,
                  static_cast<double>(y) + hints->padding);

    cairo_set_source(cr, active ? hints->af : hints->fg);
    pango_cairo_update_layout(cr, layout);
    pango_cairo_layout_path(cr, layout);
    cairo_fill(cr);

    g_object_unref(layout);
}

void launch_browser(char *browser, char *url) {
    char *browser_cmd[3] = {browser, url, nullptr};
    GError *error = nullptr;

    if (!browser) {
        g_printerr("browser not set, can't open url\n");
        return;
    }

    GPid child_pid;
    if (!g_spawn_async(nullptr, browser_cmd, nullptr, G_SPAWN_SEARCH_PATH,
                       nullptr, nullptr, &child_pid, &error)) {
        g_printerr("error launching '%s': %s\n", browser, error->message);
        g_error_free(error);
    }
    g_spawn_close_pid(child_pid);
}

void update_selection(VteTerminal *vte, SelectInfo const *select) {
    vte_terminal_unselect_all(vte);

    if (select->mode == Vi_Mode::V_M_Command) {
        return;
    }

    long const n_columns = vte_terminal_get_column_count(vte);
    long cursor_col, cursor_row, selection_x_end;
    vte_terminal_get_cursor_position(vte, &cursor_col, &cursor_row);

    vte_terminal_set_selection_block_mode(vte, select->mode ==
                                                   Vi_Mode::V_M_VisualBlock);

    if (select->mode == Vi_Mode::V_M_Visual) {
        long const begin = select->begin_row * n_columns + select->begin_col;
        long const end = cursor_row * n_columns + cursor_col;
        if (begin < end) {
            selection_x_end = cursor_col + 1;
            vte_terminal_select_text(vte, select->begin_col, select->begin_row,
                                     selection_x_end, cursor_row);
        } else {
            selection_x_end = select->begin_col + 1;
            vte_terminal_select_text(vte, cursor_col, cursor_row,
                                     selection_x_end, select->begin_row);
        }
    } else if (select->mode == Vi_Mode::V_M_VisualLine) {
        selection_x_end = n_columns;
        vte_terminal_select_text(
            vte, 0, std::min(select->begin_row, cursor_row), selection_x_end,
            std::max(select->begin_row, cursor_row));
    } else if (select->mode == Vi_Mode::V_M_VisualBlock) {
        selection_x_end = std::max(select->begin_col, cursor_col) + 1;
        vte_terminal_select_text(vte, std::min(select->begin_col, cursor_col),
                                 std::min(select->begin_row, cursor_row),
                                 selection_x_end,
                                 std::max(select->begin_row, cursor_row));
    }

    vte_terminal_copy_primary(vte);
}

long first_row(VteTerminal *vte) {
    GtkAdjustment *adjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(vte));
    return (long)gtk_adjustment_get_lower(adjust);
}

void move_to_row_start(VteTerminal *vte, SelectInfo *select, long row) {
    vte_terminal_set_cursor_position(vte, 0, row);
    update_scroll(vte);
    update_selection(vte, select);
}

long last_row(VteTerminal *vte) {
    GtkAdjustment *adjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(vte));
    return (long)gtk_adjustment_get_upper(adjust) - 1;
}

long top_row(VteTerminal *vte) {
    GtkAdjustment *adjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(vte));
    return (long)gtk_adjustment_get_value(adjust);
}

long middle_row(VteTerminal *vte) {
    GtkAdjustment *adjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(vte));
    return (long)gtk_adjustment_get_upper(adjust) - 1;
}

long bottom_row(VteTerminal *vte) {
    GtkAdjustment *adjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(vte));
    return (long)gtk_adjustment_get_value(adjust) +
           (long)vte_terminal_get_row_count(vte) - 1;
}

void copy_selection(VteTerminal *vte) {
    vte_terminal_copy_clipboard_format(vte, VTE_FORMAT_TEXT);
    vte_terminal_unselect_all(vte);
}

void overlay_show(SearchPanelInfo *info, Overlay_Mode mode, VteTerminal *vte) {
    if (vte) {
        GtkEntryCompletion *completion = gtk_entry_completion_new();
        gtk_entry_set_completion(GTK_ENTRY(info->entry), completion);
        g_object_unref(completion);

        GtkTreeModel *completion_model = create_completion_model(vte);
        gtk_entry_completion_set_model(completion, completion_model);
        g_object_unref(completion_model);

        gtk_entry_completion_set_inline_selection(completion, TRUE);
        gtk_entry_completion_set_text_column(completion, 0);
    }

    gtk_entry_set_text(GTK_ENTRY(info->entry), "");

    info->mode = mode;
    gtk_widget_show(info->entry);
    gtk_widget_grab_focus(info->entry);
}

void open_selection(char *browser, VteTerminal *vte) {
    if (!vte_terminal_get_has_selection(vte)) {
        g_printerr("no selection to open\n");
        return;
    }

    if (browser) {
        auto selection = with_unique(vte_terminal_get_selection(vte), g_free);
        if (selection && *selection) {
            launch_browser(browser, selection.get());
        }
    } else {
        g_printerr("no browser to open url\n");
    }
}

void find_urls(VteTerminal *vte, SearchPanelInfo *panel_info) {
    g_autoptr(GRegex) regex = g_regex_new(url_regex, G_REGEX_CASELESS,
                                          G_REGEX_MATCH_NOTEMPTY, nullptr);
    g_autofree char *content =
        vte_terminal_get_text_format(vte, VTE_FORMAT_TEXT);

    char const *start_pos = content;
    char const *end_pos = strchrnul(start_pos, '\n');

    for (unsigned row = 0; *start_pos != '\0';
         ++row, start_pos = end_pos + 1, end_pos = strchrnul(start_pos, '\n')) {
        auto const line_length = end_pos - start_pos;

        assert(start_pos >= content);
        assert(start_pos <= end_pos);
        assert(line_length >= 0);

        if (!line_length) { // Empty line.
            continue;
        }

        g_autoptr(GError) error = nullptr;
        g_autoptr(GMatchInfo) info = nullptr;
        g_regex_match_full(regex, start_pos, line_length, 0,
                           static_cast<GRegexMatchFlags>(0), &info, &error);

        while (g_match_info_matches(info)) {
            int pos;
            g_match_info_fetch_pos(info, 0, &pos, nullptr);
            panel_info->url_list.emplace_back(g_match_info_fetch(info, 0), pos,
                                              row);
            g_match_info_next(info, &error);
        }

        if (error) {
            g_printerr("error while matching: %s\n", error->message);
        }
    }
}

void increase_font_scale(VteTerminal *vte) {
    gdouble scale = vte_terminal_get_font_scale(vte);

    for (auto it = zoom_factors.begin(); it != zoom_factors.end(); ++it) {
        if ((*it - scale) > 1e-6) {
            vte_terminal_set_font_scale(vte, *it);
            return;
        }
    }
}

void decrease_font_scale(VteTerminal *vte) {
    gdouble scale = vte_terminal_get_font_scale(vte);

    for (auto it = zoom_factors.rbegin(); it != zoom_factors.rend(); ++it) {
        if ((scale - *it) > 1e-6) {
            vte_terminal_set_font_scale(vte, *it);
            return;
        }
    }
}

void reset_font_scale(VteTerminal *vte, gdouble scale) {
    vte_terminal_set_font_scale(vte, scale);
}

void launch_in_directory(VteTerminal *vte) {
    char const *uri = vte_terminal_get_current_directory_uri(vte);
    if (!uri) {
        g_printerr("no directory uri set\n");
        return;
    }
    auto dir = with_unique(g_filename_from_uri(uri, nullptr, nullptr), g_free);
    char term[] = PROGRAM_NAME; // maybe this should be argv[0]
    char *cmd[] = {term, nullptr};
    g_spawn_async(dir.get(), cmd, nullptr, G_SPAWN_SEARCH_PATH, nullptr,
                  nullptr, nullptr, nullptr);
}

void enter_command_mode(VteTerminal *vte, SelectInfo *select) {
    vte_terminal_disconnect_pty_read(vte);
    select->mode = Vi_Mode::V_M_Command;
    vte_terminal_get_cursor_position(vte, &select->origin_col,
                                     &select->origin_row);
    update_selection(vte, select);
}

gboolean modify_key_feed(GdkEventKey *event, KeybindInfo *info,
                         std::map<int, char const *> const &table) {
    if (info->config.modify_other_keys) {
        unsigned int keyval = gdk_keyval_to_lower(event->keyval);
        auto entry = table.find((int)keyval);

        if (entry != table.end()) {
            vte_terminal_feed_child(info->vte, entry->second, -1);
            return TRUE;
        }
    }
    return FALSE;
}

template <typename F>
static void move_backward(VteTerminal *vte, SelectInfo *select, F is_word) {
    long cursor_col, cursor_row;
    vte_terminal_get_cursor_position(vte, &cursor_col, &cursor_row);

    auto content = get_text_range(vte, cursor_row, 0, cursor_row, cursor_col);

    if (!content) {
        return;
    }

    long length;
    gunichar *codepoints =
        g_utf8_to_ucs4(content.get(), -1, nullptr, &length, nullptr);

    if (!codepoints) {
        return;
    }

    // sanitize cursor_col, if the user moved the cursor using free movement
    // beyond the end of the line input the matching be off for one 'word'.
    cursor_col = length;

    long count = get_count(select);

    bool in_word = false;

    long i = length - 1;
    for (; i >= 0; i--) {
        if (!is_word(codepoints[i - 1])) {
            if (in_word) {
                if (--count == 0) {
                    break;
                }
                in_word = false;
            }
        } else {
            in_word = true;
        }
    }
    cursor_col = std::max<long>(cursor_col - (length - i), 0);
    vte_terminal_set_cursor_position(vte, cursor_col, cursor_row);
    update_selection(vte, select);

    g_free(codepoints);
}

template <typename F>
static void move_forward(VteTerminal *vte, SelectInfo *select, F is_word,
                         bool goto_word_end) {
    long cursor_col, cursor_row;
    vte_terminal_get_cursor_position(vte, &cursor_col, &cursor_row);

    long const end_col = vte_terminal_get_column_count(vte) - 1;

    auto content =
        get_text_range(vte, cursor_row, cursor_col, cursor_row, end_col);

    if (!content) {
        return;
    }

    long length;
    gunichar *codepoints =
        g_utf8_to_ucs4(content.get(), -1, nullptr, &length, nullptr);

    if (!codepoints) {
        return;
    }

    // prevent going past the end (get_text_range adds a \n)
    if (codepoints[length - 1] == '\n') {
        length--;
    }

    long count = get_count(select);

    bool end_of_word = false;

    if (!goto_word_end) {
        for (long i = 1; i < length; i++) {
            if (is_word(codepoints[i - 1])) {
                if (end_of_word) {
                    if (--count == 0) {
                        break;
                    }
                    end_of_word = false;
                }
            } else {
                end_of_word = true;
            }
            cursor_col++;
        }
    } else {
        for (long i = 2; i <= length; i++) {
            cursor_col++;
            if (is_word(codepoints[i - 1]) && !is_word(codepoints[i])) {
                if (--count == 0) {
                    break;
                }
            }
        }
    }
    vte_terminal_set_cursor_position(vte, cursor_col, cursor_row);
    update_selection(vte, select);

    g_free(codepoints);
}

void update_scroll(VteTerminal *vte) {
    GtkAdjustment *adjust = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(vte));
    double const scroll_row = gtk_adjustment_get_value(adjust);
    long const n_rows = vte_terminal_get_row_count(vte);
    long cursor_row;
    vte_terminal_get_cursor_position(vte, nullptr, &cursor_row);

    if ((double)cursor_row < scroll_row) {
        gtk_adjustment_set_value(adjust, (double)cursor_row);
    } else if (cursor_row - n_rows >= (long)scroll_row) {
        gtk_adjustment_set_value(adjust, (double)(cursor_row - n_rows + 1));
    }
}

bool is_word_char(gunichar c) {
    static char const *word_char_ascii_punct = "-,./?%&#_=+@~";
    return g_unichar_isgraph(c) &&
           (g_unichar_isalnum(c) ||
            (g_unichar_ispunct(c) &&
             (c >= 0x80 || strchr(word_char_ascii_punct, (int)c) != NULL)));
}

void draw_rectangle(cairo_t *cr, double x, double y, double height,
                    double width, double radius) {
    double a = x, b = x + height, c = y, d = y + width;
    cairo_arc(cr, a + radius, c + radius, radius, 2 * (M_PI / 2),
              3 * (M_PI / 2));
    cairo_arc(cr, b - radius, c + radius, radius, 3 * (M_PI / 2),
              4 * (M_PI / 2));
    cairo_arc(cr, b - radius, d - radius, radius, 0 * (M_PI / 2),
              1 * (M_PI / 2));
    cairo_arc(cr, a + radius, d - radius, radius, 1 * (M_PI / 2),
              2 * (M_PI / 2));
    cairo_close_path(cr);
}

GtkTreeModel *create_completion_model(VteTerminal *vte) {
    GtkListStore *store = gtk_list_store_new(1, G_TYPE_STRING);

    long end_row, end_col;
    vte_terminal_get_cursor_position(vte, &end_col, &end_row);
    auto content = get_text_range(vte, 0, 0, end_row, end_col);

    if (!content) {
        g_printerr("no content returned for completion\n");
        return GTK_TREE_MODEL(store);
    }

    auto less = [](char const *a, char const *b) {
        return strcmp(a, b) < 0;
    };
    std::set<char const *, decltype(less)> tokens(less);

    for (char *s_ptr = content.get(), *saveptr;; s_ptr = nullptr) {
        char const *token = strtok_r(s_ptr, " \n\t", &saveptr);
        if (!token) {
            break;
        }
        tokens.insert(token);
    }

    for (char const *token: tokens) {
        GtkTreeIter iter;
        gtk_list_store_append(store, &iter);
        gtk_list_store_set(store, &iter, 0, token, -1);
    }

    return GTK_TREE_MODEL(store);
}

char *check_match(VteTerminal *vte, GdkEventButton *event) {
    int tag;

    return vte_terminal_match_check_event(vte, (GdkEvent *)event, &tag);
}

template <typename T>
std::optional<T>
get_config(T (*get)(GKeyFile *, char const *, char const *, GError **),
           GKeyFile *config, char const *group, char const *key) {
    GError *error = nullptr;
    std::optional<T> value = get(config, group, key, &error);
    if (error) {
        g_error_free(error);
        return {};
    }
    return value;
}
